Skip to main content

Introduction

Connected Objects are a powerful evolution of Network Objects.
They comes with an enhanced and configurable RMI system.

See the next page to know how to start with connected objects.
Make sure you understood the Network Objects concept in order not to get lost while reading this part.

Some reminders:

  • An Engine is a connection in a Linkit Network. It can either be the server or a client connection.
  • The linkit's network operates in a star topology. In the diagrams below you'll see arrows directly pointing toward engines. This is to simplify the diagrams as the topology is not important here.

Summary

Roughly, Connected objects are Network Objects that gets handled by an RMI system. This RMI system follows a contract that says what should happen when a connected object's method is invoked. The contract is the most important part of the Connected Object System. By configuring a contract, you specify how your connected objects will behave over the network.
A language has been made in order to simply configure a contract.

The Features list below will introduce to things you can do with connected objects.
Some features are not or not directly concerned by the contract.

Features

Invocation Flow Control (agreements)

When a method of a connected object gets called, the RMI system will follow it's bound agreement. The agreement specify over which engines the method invocation must also be invoked, and which engine is appointed to return the final result of the call.

Shared/Synchronized Objects

class MyWrapper[T](value: T = null) {
def set(t: T): T = {
val last = value
value = t
print(s"Changed value to '$value'")
last
}
def get: T = value
}

In this example, we create a fully synchronized mutable object by specifying in our contract that when the set(T): T method is invoked on an engine, the other engines that contains the same wrapper object also gets a call with the right parameters.

For our getter, we just handle it as a regular method, as we are sure that our local value is the same on other engines, we don't want the invocation to get performed on our remote engines.

Engine 1
Engine 1 
Server Engine
Server Engi...
Engine 3
Engine 3
Application
Application
myWrapper.set("hey")
myWrapper.set("hey")
> Changed value to 'Hey'
> Changed value to 'Hey'
Current Engine
Current Engine
O
O
O
O
O
O
O
O
> Changed value to 'Hey'
> Changed value to 'Hey'
> Changed value to 'Hey'
> Changed value to 'Hey'
> Changed value to 'Hey'
> Changed value to 'Hey'
Legend :
Legend : 
Invocation
Invocation

Therefore, we can simulate a "cloud", a "shared" object being accessed from engines and which internal state is completely synchronized on each side.

Select specific engines and apply simple condition depending on the invocation context

You can define simple constraints on your agreements and then having different invocation flows depending on the initial method execution context:

In this example, we have different invocation flow depending on whether the object on which the method is initially called is the original object or not (in red on the diagram)

class MyObject {
/**
* This method prints a param and return the current JVM version.
* */
def doSomething(param: String): String = {
print(s"invocation called ! ($param)")
System.getProperty("java.version")
}
}
O
O
Server Engine
Server Engi...
Engine 3
(owner)
Engine 3...
Application
Application
println(myObject.doSomething("Hello !"))
println(myObject.doSomething("Hello !"...
> 13.0.2
> 13.0.2
Current Engine
Current Engine
O
O
O
O
Java 16.1.2
Java 16.1.2
Java 13.0.2
Java 13.0.2
Java 13.1.4
Java 13.1.4
> invocation called ! (Hello !)
> invocation called ! (Hello !)
O
O
Server Engine
Server Engi...
Engine 3
Engine 3
Application
Application
println(myObject.doSomething("Hello !"))
println(myObject.doSomething("Hello !"...
> 13.1.4
> 13.1.4
Current Engine (owner)
Current Engine (ow...
O
O
O
O
Java 16.1.2
Java 16.1.2
Java 13.0.2
Java 13.0.2
Java 13.1.4
Java 13.1.4
> invocation called ! (Hello !)
> invocation called ! (Hello !)
When current is not owner:
When current is not owner: 
When current is owner:
When current is owner: 
Legend :
Legend : 
Invocation
Invocation
Invocation + return
Invocation + return

Contract says:
Contract says: 
For class MyObject:
For class MyObject: 
For method doSomething:
For method doSomething: 
If current engine is the object's owner:
If current engine is the object's owner...
Execute on server and current then keep the current's method invocation result
Execute on server and current then keep...
Else:
Else: 
Execute on owner engine only and keep owner engine's method invocation result
Execute on owner engine only and keep o...
note

Connected Object owners are the engines that initially posted their objects over the network.

Mirror Objects and Remote Implementation control

This is actually one of the most powerful possibility when using Connected Objects.

A Mirror Object is a specific kind of connected object where a Mirroring object is an "abstract object" in which all method invocations are performed on a remote Mirrored object.
This way, a Mirroring object can be a trait, interface or an abstract class and the Mirrored object is a remote implementation of the class.

Example : We want to make an api to our server's database by defining a DAO[T] trait.
The client only have the DAO trait on his side, and the server has the concrete class PlayerDAO that implements DAO[Player]

DAO.scala
trait DAO[A] {
def get(id: Long): A
def getAll: List[A]
def save(a: A): Unit
def update(a: A): Unit
def delete(a: A): Unit
}

Now on the client side, when we retrieve our DAO object (see Getting Started page to understand the first line) we will be able to use a DAO[Player] (Mirroring) object where its implementation is on the server-side (Mirrored).

on client-side:
val playerDAO: DAO[Player] = cache.findObject(0).get //this line retrieves the playerDAO object from server's cacheval player = Player(name="Rico", id=78)playerDAO.save(player)playerDAO.get(id=78) == player //true

Here's what happened over the network :

Server Engine
Server Engi...
Application
Application
playerDAO.save(player)
playerDAO.get(id=78)
playerDAO.save(player)...
Current Engine
Current Engine
Contract says:
Contract says: 
For class DAO:
For class DAO: 
For all methods:
For all methods: 
O
O
O
O
DAO[Player] trait (Mirroring Object)
DAO[Player] trai...
PlayerDAO class (Mirrored Object)
PlayerDAO c...
- Perform the invocation on the server engine and so, use it's invocation return value
- Perform the invocation...
For class PlayerDAO:
For class PlayerDAO: 
Only on server-side
Only on server-side
Use stub class 'DAO[T]' when other engines access this object when if it's a "Mirrored Object"
Use stub class 'DAO[T]' when ot...
Legend :
Legend : 
Invocation + return
Invocation + return

How does it work ?

The Linkit Framework generates an implementation class for any Connected Object.
This allows the RMI System to handle the object's methods invocation by overriding all methods of the object's class.
When a method is invoked on a connected object, its generated implementation will follow the associated contract.

For Mirroring Objects, it's the same principle;
When the DAO object got retrieved at first line, the Linkit Framework followed the contract on server-side that said "Use stub class 'DAO[T]' when other engines access this object if it's a mirrored object" and so generated a concrete implementation for `DAO[T]` on client-side.

Know who is the invocation origin

Using the utility method ExecutorEngine.currentEngine, you can know who is the origin of the execution of a method. This is actually very powerful combined with the possibility of making Mirrored Objects as you can choose to perform different actions or checks depending on the engine that accesses a method.

class MyObject {
/**
* This method prints who originally invocated the method.
* */
def printInvoker(): Unit = {
val origin = ExecutorEngine.currentEngine
print(s"invocation called ! this RMI has been triggered from " + origin.identifier)
}
}
O
O
Server Engine
Server Engi...
Application
Application
myObject.printInvoker()
myObject.printInvoker()
Current Engine
(Engine "client-2")
Current Engine...
O
O
Java 13
Java 13
Contract says:
Contract says: 
For class MyObject:
For class MyObject: 
For method doSomething:
For method doSomething: 
- Spread the invocation on the server
- Spread the invocation on...
> invocation called ! this RMI has been triggered from "client-2"
> invocation called ! this RMI has been tri...
Legend :
Legend : 
Invocation
Invocation

Static accesses (Work In Progress)

You can also access to statics of a class and thus define a contract for static methods.

You can open your own StaticAccessor and apply a custom contract for static methods, or access to a specific engine's static accessor to call static methods on his side.

Example :

  • We want to know the time of the server we are connected on :
val serverDate = new Date(network.serverEngine.statics[System].currentTimeMillis())

To access a class (In this example, we access to the System class), the class must be allowed by the contract on remote-side.
Some methods can be hided by the contract as well :

network.serverEngine.statics[System].exit(0) //throws HiddenMethodInvocationException.

Apply contract on parameters and return values

Mirror / connect parameters and return values

It's possible to propagate object connection by specifying in a contract that parameter and / or return value of a method should be converted to a connected object.

Example :
We want to develop a game with players evolving on a map.

Here is our Player class :

mygame.Player.scala - implementation on both side
trait Player {
val name: String
def shoot(): Unit
def hit(damage: Double): Unit
def move(x: Int, y: Int): Unit
}

And our map trait :

mygame.Map.scala - implementation on server-side
trait Map {
@throws[Exception]("if a client already created his player")
def newPlayer(name: String): Player
def getPlayer(name: String): Player //returns the same Player instance as returned by #newPlayer
}

Here is our contract (defined here using the bhv language)

game_network.bhv
import mygame.Player
import mygame.Map
describe Player {
foreach method enable following broadcast
}
describe Map {
//connect keyword in front of a parameter or return type stipulates that the value must be connected.
newPlayer(String): connect Player
}

On server-side, we initially create one mirrored object which is a MapImpl object.
On client-side, we retrieve the map object, and we initialize our player using Map#newPlayer.
By specifying in our contract that the return value of Map#newPlayer must be converted to a connected Player, the client will retrieve a connected instance of Player.
And as all method invocation of Player are to be broadcast, Player objects will be completely synchronized, and then for example, when a client's player moves or shoots, all other clients, and the server, will be aware that the player changed.

Also Connect field values of connected objects

it's also possible to specify which field of a connected object we want to be connected as well:

example.bhv
describe Example {
connect field1
mirror field2
}

Apply modifiers